In this project, we are going to develop very basic methodologies for
sentiment analysis including Logistic Regression,
Naive Bayes, and Vector Space
Classification and test their performance on a dataset I have
found online. There are multiple R packages that are needed for this
project. You can use the code install.packages(“Package
Name”) to install the packages listed below.
Loading & Preparing the data
loading the DigiKala data related to iPhone that I have found
online.
dat = read.csv("Data.txt", header = T, encoding = "UTF-8")
#at=read_excel("Data.xlsx")
dat=na.omit(dat)
By considering the label “1” as “Positive” and the rest of the labels
as “Negative”, we are preparing the data for creating a model for
detecting the sentiments from a comment.
Corpus=lookup_emoji(dat$Text, text_field = "text")
Text = Corpus$text
Emoji = Corpus$emoji
Y=as.numeric(dat$Suggestion==1)
Defining functions for text cleaning, building up a dictionary, and
extracitng features. First, the function that prepares the text for the
analysis.
RefineText<- function(tex){
n=length(tex)
TextAd=c()
#progress bar
pb = txtProgressBar(min = 0, max = n, initial = 0,
style = 3, width = 50, char = "=")
for (i in 1:n) {
tex[i] = RefineChars(tex[i])
tex[i] = gsub("[\r\n]", "", tex[i])
tex[i] = gsub('[[:punct:] ]+',' ', tex[i])
tex[i] = RefineChars(tex[i])
#,"@","$","%","&","*","،",".....","..."
if (!(is.na(tex[i]))){
if (all(tex[i]!=c(""," ","،","،,",""))){
TextAd[i] = PerStem(tex[i], NoEnglish = F, NoNumbers = F,
NoStopwords = T, NoPunctuation = T,
StemVerbs = T, NoPreSuffix = T,
Context = T, StemBrokenPlurals = T,
Transliteration = F)
}
}
setTxtProgressBar(pb,i)
}
return(TextAd)
}
Now, it is time to build up a dictionary.
BuildFreqs <- function(corpus,y) {
n = length(y)
d=1 # dictionary word numerator
freqs=matrix(c(0,0),ncol = 2) #frequencies for each word
colnames(freqs)=c("Neg","Pos")
Dict=c()
t=c()
e=c()
tex = corpus$text
emoji = corpus$emoji
#refining Text removing numbers, stop words, punctuation, and so
#Progress bar
pb = txtProgressBar(min = 0, max = n, initial = 0, style = 3, width = 50, char = "=")
for (i in 1:n) {
t = sort(table(unlist(strsplit(tex[i], " "))), decreasing = TRUE)
#Building freqs for words
for (j in 1:length(t)) {
cond = (Dict == names(t)[j])
if (any(cond)) {
wh = which(cond)
freqs[wh+1,(y[i]+1)] = freqs[wh+1,(y[i]+1)]+t(j) # Adding Frequency
} else if(is.null(names(t))==F) {
Dict[d] = names(t)[j]
if (y[i]==1) {
freqs = rbind(freqs,c(0,1)) # Defining Frequency
} else {
freqs = rbind(freqs,c(1,0)) # Defining Frequency
}
d = d+1
}
}
if(is.null(emoji[[i]])==F){
e = table(emoji[i])
#Building frequencies for emojis
for (j in 1:length(e)) {
cond = (Dict == names(e)[j])
if (any(cond)) {
wh = which(cond)
freqs[wh+1,(y[i]+1)] = freqs[wh+1,(y[i]+1)]+e[j]
# Adding Frequency
} else if(is.null(names(e))==F) {
Dict[d] = names(e)[j]
if (y[i]==1) {
freqs = rbind(freqs,c(0,1)) # Defining Frequency
} else {
freqs = rbind(freqs,c(1,0)) # Defining Frequency
}
d = d+1
}
}
}
setTxtProgressBar(pb,i)
}
Dictionary=list("Words"=Dict,"Frequencies"=freqs[-1,])
return(Dictionary)
}
Finally, a function that uses the dictionary to extract features.
ExtractFeatures <- function(corpus, dict){
tex = corpus$text
emoji = corpus$emoji
Words=dict$Words
dict$Frequencies = dict$Frequencies/rowSums(dict$Frequencies)
Frequencies=dict$Frequencies
#Progress bar
n = length(tex)
pb = txtProgressBar(min = 0, max = n, initial = 0, style = 3,
width = 50, char = "=")
x = matrix(rep(0,n*3),nrow =n ,ncol=3)
for (i in 1:n) {
wordlist = names(sort(table(unlist(strsplit(tex[i], " "))),
decreasing = TRUE))
if(is.null(emoji[[i]])==F){append(wordlist,table(emoji[i]))}
x[i,1] = 1 #bias term is set to 1
# loop through each word in the list of words
for (word in wordlist){
if (any(Words==word)) {
# increment the word count for the neutral label 0
x[i,2] = x[i,2]+Frequencies[which(Words==word),1]
# increment the word count for the positive label 1
x[i,3] = x[i,3]+Frequencies[which(Words==word),2]
}
}
setTxtProgressBar(pb,i)
}
return(x)
}
Using the functions defined, we are going to clean our Persian text,
then build a dictionary, and extract features from the text.
TextAd=RefineText(Text)
Dictionary=BuildFreqs(list("text"=TextAd,"emoji"=Emoji),Y)
X = ExtractFeatures(list("text"=TextAd,"emoji"=Emoji), Dictionary)
Checking the features interpretability
To see, weather it is possible to to classify our text using the
features we built, We must check the below scatter plot.
mylabel <- c("Train Pos", "Train Neg")
colors <- c("blue", "red")
xlabel = "Sum of Negative Words"
ylabel = "Sum of Positive Words"
plot_ly(x=X[,2],y=X[,3],
symbols = c('x','o'),
marker = list(size = 10),
text= c(1:length(Y)), hoverinfo = "text",
type="scatter",mode="markers", color=as.factor(Y),colors = c("red", "green")) %>%
layout(title = "Comments by the Features built",
autosize = F, width = 920, height = 500)
The first classification method that is widely being used as a
trivial and easy-to-implement methodology is Naive Bayes. In the
following lines of code, we are going to build two functions by which we
can use this method for our data.
ExtractSense=function(corpus, dict){
tex = corpus$text
emoji = corpus$emoji
Words=dict$Words
Frequencies=dict$Frequencies
d=length(dict$Words)
n=length(tex)
#Laplacian Smoothing
Freqs=1/(colSums(Frequencies)+d)*(t(Frequencies)+1)
Freqs=t(Freqs)
p_w_pos=Freqs[,2]
p_w_neg=Freqs[,1]
loglikelihood = log(p_w_pos/p_w_neg)
p=rep(0,n)
pb = txtProgressBar(min = 0, max = n, initial = 0, style = 3,
width = 50, char = "=")
for(i in 1:n){
wordlist = sort(table(unlist(strsplit(tex[i], " "))), decreasing = TRUE)
if(is.null(emoji[[i]])==F){append(wordlist,table(emoji[i]))}
for (j in 1:length(wordlist)){
if (any(Words == names(wordlist)[j])) {
p[i]=p[i]+wordlist[j]*loglikelihood[Words == names(wordlist)[j]]
}
}
setTxtProgressBar(pb,i)
}
return(p)
}
And, the predictior function will be defined like this:
Now, we want to use this method on our data to build a feature called
“Sense” that measures the intensity of each comment alongside their
polarity.
Sense=ExtractSense(list("text"=TextAd,"emoji"=Emoji), Dictionary)
We can see below how this feature looks like.
plot_ly(x=1:length(Sense),y=Sense, type = 'bar') %>%
layout(title = "Overal Polarity of All the Comments",
plot_bgcolor='#e5ecf6',autosize = T, width = 880, height = 500)
The accuracy of Naive Bayes classifier on the train dataset can be
calculated as follow.
NB=NaiveBayesPredictor(Sense,Y)
yhat=NB$Yhat
NBAccuracy=c()
NBAccuracy["Positive"] =
mean(na.omit(Y[Y==1]==yhat[Y==1]))
NBAccuracy["Negative"] =
mean(na.omit(Y[Y==0]==yhat[Y==0]))
cat(paste0("Overal Accuracy: \n"),mean(yhat==Y),
paste0("\nAccuracy for different sentiments: \n"), names(NBAccuracy),
paste0(" \n"),NBAccuracy)
Overal Accuracy:
0.8730451
Accuracy for different sentiments:
Positive Negative
0.9286314 0.7224118
The second classifier is based on vector space models. We are going
to build such a classifier in the following lines of code.
VectorSpaceModel= function(x,y){
pos_center=colMeans(x[y==1,])
neg_center=colMeans(x[y==0,])
centers=cbind(pos_center[-1],neg_center[-1])
dot_prods=x[,-1]%*%centers
normX=apply(x[,-1],1,function(x)(sqrt(sum(x^2))))
normCenter=apply(centers,2,function(x)(sqrt(sum(x^2))))
angel_cosine=as.matrix(1/normX)%*%t(as.matrix(1/normCenter))*(dot_prods)
yhat=apply(angel_cosine, 1, function(x) which(max(x)==x))
yhat[yhat==2]=0
return(list("Yhat"=yhat,"Centers"=centers))
}
The next function is going to calculates (predicts) the polarity for
each comment. Plus, if we input the dependent variable (y) as well it
gives us the accuracy of the predictions.
VectorSpacePredictor= function(x,y=NA,centers){
dot_prods=x[,-1]%*%centers
normX=apply(x[,-1],1,function(x)(sqrt(sum(x^2))))
normCenter=apply(centers,2,function(x)(sqrt(sum(x^2))))
angel_cosine=as.matrix(1/normX)%*%t(as.matrix(1/normCenter))*(dot_prods)
yhat=apply(angel_cosine, 1, function(x) which(max(x)==x))
yhat[yhat==2]=0
accuracy= NA
if(is.na(y)==FALSE && length(y)==length(yhat)){
accuracy = mean(y==yhat)
}
return(list("Yhat"=yhat,"Accuracy"=accuracy))
}
Train accuracy for Vector Space classifier is as follows.
fitVS=VectorSpaceModel(X,Y)
yhat=fitVS$Yhat
VSAccuracy=c()
VSAccuracy["Positive"] =
mean(na.omit(Y[Y==1]==yhat[Y==1]))
VSAccuracy["Negative"] =
mean(na.omit(Y[Y==0]==yhat[Y==0]))
cat(paste0("Overal Accuracy: \n"),mean(yhat==Y),
paste0("\nAccuracy for different sentiments: \n"), names(VSAccuracy),
paste0(" \n"),VSAccuracy)
Overal Accuracy:
0.9024839
Accuracy for different sentiments:
Positive Negative
0.9601175 0.7463026
The logistic regression classifier accuracy on this dataset can be
calculated as follow.
fitGLM=glm(Y~X[,-1],family="binomial")
yhat=round(predict(fitGLM,as.data.frame(X),type="response"),0)
GLMAccuracy=c()
GLMAccuracy["Positive"] =
mean(na.omit(Y[Y==1]==yhat[Y==1]))
GLMAccuracy["Negative"] =
mean(na.omit(Y[Y==0]==yhat[Y==0]))
cat(paste0("Overal Accuracy: \n"),mean(yhat==Y),
paste0("\nAccuracy for different sentiments: \n"), names(GLMAccuracy),
paste0(" \n"),GLMAccuracy)
Overal Accuracy:
0.9101503
Accuracy for different sentiments:
Positive Negative
0.972712 0.7406143
It is not obvious which method performs better on this dataset. So,
we run a simulation of 1000 iterations each time we are going to
randomly split the dataset into train and test. Then we build our models
based on the train sets and test their performances on the test sets.
the results we be stored in a matrix and after the simulation will be
visualized.
So, the simulation for the defined methodologies is going to be like
this.
So, in the following lines of code, we are going to build a box plot
to compare the error of each method.
fig <- plot_ly(type = 'box')
fig <- fig %>% add_boxplot(y = Error[,1], jitter = 0.3, pointpos = -1.8, boxpoints = 'all',
marker = list(color = 'rgb(7,40,89)'),
line = list(color = 'rgb(7,40,89)'),
name = "Vector Spcae Classifier")
fig <- fig %>% add_boxplot(y = Error[,2], jitter = 0.3, pointpos = -1.8, boxpoints = 'all',
marker = list(color = 'rgb(9,56,125)'),
line = list(color = 'rgb(9,56,125)'),
name = "Naive Bayes Classifier")
fig <- fig %>% add_boxplot(y = Error[,3], jitter = 0.3, pointpos = -1.8, boxpoints = 'all',
marker = list(color = 'rgb(11,64,150)'),
line = list(color = 'rgb(11,64,150)'),
name = "Logistic Regression Classifier")
fig %>% layout(autosize = F, width = 920, height = 500)
It seems obvious now that vector space classifier performs slightly
better than naive Bayes and significantly better than Logistic
regression classifier.
LS0tDQp0aXRsZTogIlNlbnRpbWVudCBBbmFseXNpcyBmb3IgUGVyc2lhbiBUZXh0Ig0Kb3V0cHV0OiBodG1sX25vdGVib29rDQotLS0NCg0KDQpJbiB0aGlzIHByb2plY3QsIHdlIGFyZSBnb2luZyB0byBkZXZlbG9wIHZlcnkgYmFzaWMgbWV0aG9kb2xvZ2llcyBmb3Igc2VudGltZW50IGFuYWx5c2lzIGluY2x1ZGluZyAqKkxvZ2lzdGljIFJlZ3Jlc3Npb24qKiwNCioqTmFpdmUgQmF5ZXMqKiwgYW5kICoqVmVjdG9yIFNwYWNlIENsYXNzaWZpY2F0aW9uKiogYW5kIHRlc3QgdGhlaXIgcGVyZm9ybWFuY2Ugb24gYSBkYXRhc2V0IEkgaGF2ZSBmb3VuZCBvbmxpbmUuIFRoZXJlIGFyZSBtdWx0aXBsZSBSIHBhY2thZ2VzIHRoYXQgYXJlIG5lZWRlZCBmb3IgdGhpcyBwcm9qZWN0LiBZb3UgY2FuIHVzZSB0aGUgY29kZSAqKmluc3RhbGwucGFja2FnZXMoIlBhY2thZ2UgTmFtZSIpKiogdG8gaW5zdGFsbCB0aGUgcGFja2FnZXMgbGlzdGVkIGJlbG93Lg0KDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbGlicmFyeShyd2hhdHNhcHApDQpsaWJyYXJ5KFBlcnNpYW5TdGVtbWVyKQ0KbGlicmFyeShwbG90bHkpDQpsaWJyYXJ5KGdncGxvdDIpDQpsaWJyYXJ5KHdvcmRjbG91ZDIpDQpgYGANCg0KDQojIExvYWRpbmcgJiBQcmVwYXJpbmcgdGhlIGRhdGENCg0KbG9hZGluZyB0aGUgRGlnaUthbGEgZGF0YSByZWxhdGVkIHRvIGlQaG9uZSB0aGF0IEkgaGF2ZSBmb3VuZCBvbmxpbmUuDQpgYGB7ciB3YXJuaW5nPUZBTFNFfQ0KZGF0ID0gcmVhZC5jc3YoIkRhdGEudHh0IiwgaGVhZGVyID0gVCwgZW5jb2RpbmcgPSAiVVRGLTgiKQ0KI2RhdD1yZWFkX2V4Y2VsKCJEYXRhLnhsc3giKQ0KZGF0PW5hLm9taXQoZGF0KQ0KYGBgDQoNCg0KQnkgY29uc2lkZXJpbmcgdGhlIGxhYmVsICIxIiBhcyAiUG9zaXRpdmUiIGFuZCB0aGUgcmVzdCBvZiB0aGUgbGFiZWxzIGFzICJOZWdhdGl2ZSIsIHdlIGFyZSBwcmVwYXJpbmcgdGhlIGRhdGEgZm9yIGNyZWF0aW5nIGEgbW9kZWwgZm9yIGRldGVjdGluZyB0aGUgc2VudGltZW50cyBmcm9tIGEgY29tbWVudC4NCmBgYHtyfQ0KQ29ycHVzPWxvb2t1cF9lbW9qaShkYXQkVGV4dCwgdGV4dF9maWVsZCA9ICJ0ZXh0IikNClRleHQgPSBDb3JwdXMkdGV4dA0KRW1vamkgPSBDb3JwdXMkZW1vamkNClk9YXMubnVtZXJpYyhkYXQkU3VnZ2VzdGlvbj09MSkNCmBgYA0KDQoNCkRlZmluaW5nIGZ1bmN0aW9ucyBmb3IgdGV4dCBjbGVhbmluZywgYnVpbGRpbmcgdXAgYSBkaWN0aW9uYXJ5LCBhbmQgZXh0cmFjaXRuZyBmZWF0dXJlcy4gRmlyc3QsIHRoZSBmdW5jdGlvbiB0aGF0IHByZXBhcmVzIHRoZSB0ZXh0IGZvciB0aGUgYW5hbHlzaXMuDQpgYGB7cn0NClJlZmluZVRleHQ8LSBmdW5jdGlvbih0ZXgpew0KICBuPWxlbmd0aCh0ZXgpDQogIFRleHRBZD1jKCkNCiAgDQogICNwcm9ncmVzcyBiYXINCiAgcGIgPSB0eHRQcm9ncmVzc0JhcihtaW4gPSAwLCBtYXggPSBuLCBpbml0aWFsID0gMCwNCiAgICAgICAgICAgICAgICAgICAgICBzdHlsZSA9IDMsIHdpZHRoID0gNTAsIGNoYXIgPSAiPSIpIA0KICANCiAgZm9yIChpIGluIDE6bikgew0KICAgIHRleFtpXSA9IFJlZmluZUNoYXJzKHRleFtpXSkgDQogICAgdGV4W2ldID0gZ3N1YigiW1xyXG5dIiwgIiIsIHRleFtpXSkNCiAgICB0ZXhbaV0gPSBnc3ViKCdbWzpwdW5jdDpdIF0rJywnICcsIHRleFtpXSkNCiAgICB0ZXhbaV0gPSBSZWZpbmVDaGFycyh0ZXhbaV0pDQogICAgICAgIA0KICAgICMsIkAiLCIkIiwiJSIsIiYiLCIqIiwi2IwiLCIuLi4uLiIsIi4uLiINCiAgICBpZiAoIShpcy5uYSh0ZXhbaV0pKSl7DQogICAgICBpZiAoYWxsKHRleFtpXSE9YygiIiwiICIsItiMIiwi2IwsIiwi4oCMIikpKXsNCiAgICAgICAgVGV4dEFkW2ldID0gUGVyU3RlbSh0ZXhbaV0sIE5vRW5nbGlzaCA9IEYsIE5vTnVtYmVycyA9IEYsDQogICAgICAgICAgICAgICAgICAgICAgICBOb1N0b3B3b3JkcyA9IFQsIE5vUHVuY3R1YXRpb24gPSBULA0KICAgICAgICAgICAgICAgICAgICAgICAgU3RlbVZlcmJzID0gVCwgTm9QcmVTdWZmaXggPSBULA0KICAgICAgICAgICAgICAgICAgICBDb250ZXh0ID0gVCwgU3RlbUJyb2tlblBsdXJhbHMgPSBULA0KICAgICAgICAgICAgICAgICAgICBUcmFuc2xpdGVyYXRpb24gPSBGKQ0KICAgICAgfQ0KICAgIH0NCiAgICBzZXRUeHRQcm9ncmVzc0JhcihwYixpKQ0KICB9DQpyZXR1cm4oVGV4dEFkKQ0KfQ0KYGBgDQoNCg0KTm93LCBpdCBpcyB0aW1lIHRvIGJ1aWxkIHVwIGEgZGljdGlvbmFyeS4NCmBgYHtyfQ0KQnVpbGRGcmVxcyA8LSBmdW5jdGlvbihjb3JwdXMseSkgew0KICBuID0gbGVuZ3RoKHkpDQpkPTEgIyBkaWN0aW9uYXJ5IHdvcmQgbnVtZXJhdG9yDQpmcmVxcz1tYXRyaXgoYygwLDApLG5jb2wgPSAyKSAjZnJlcXVlbmNpZXMgZm9yIGVhY2ggd29yZA0KY29sbmFtZXMoZnJlcXMpPWMoIk5lZyIsIlBvcyIpDQpEaWN0PWMoKSANCnQ9YygpDQplPWMoKQ0KdGV4ID0gY29ycHVzJHRleHQNCmVtb2ppID0gY29ycHVzJGVtb2ppDQojcmVmaW5pbmcgVGV4dCByZW1vdmluZyBudW1iZXJzLCBzdG9wIHdvcmRzLCBwdW5jdHVhdGlvbiwgYW5kIHNvDQojUHJvZ3Jlc3MgYmFyDQpwYiA9IHR4dFByb2dyZXNzQmFyKG1pbiA9IDAsIG1heCA9IG4sIGluaXRpYWwgPSAwLCBzdHlsZSA9IDMsIHdpZHRoID0gNTAsIGNoYXIgPSAiPSIpDQpmb3IgKGkgaW4gMTpuKSB7DQogIHQgPSBzb3J0KHRhYmxlKHVubGlzdChzdHJzcGxpdCh0ZXhbaV0sICIgIikpKSwgZGVjcmVhc2luZyA9IFRSVUUpDQogIA0KICAjQnVpbGRpbmcgZnJlcXMgZm9yIHdvcmRzDQogIGZvciAoaiBpbiAxOmxlbmd0aCh0KSkgew0KICAgIGNvbmQgPSAoRGljdCA9PSBuYW1lcyh0KVtqXSkNCiAgICBpZiAgKGFueShjb25kKSkgew0KICAgICAgd2ggPSB3aGljaChjb25kKQ0KICAgICAgZnJlcXNbd2grMSwoeVtpXSsxKV0gPSBmcmVxc1t3aCsxLCh5W2ldKzEpXSt0KGopICMgQWRkaW5nIEZyZXF1ZW5jeQ0KICAgIH0gZWxzZSBpZihpcy5udWxsKG5hbWVzKHQpKT09Rikgew0KICAgICAgRGljdFtkXSA9IG5hbWVzKHQpW2pdDQogICAgICBpZiAoeVtpXT09MSkgew0KICAgICAgICBmcmVxcyA9IHJiaW5kKGZyZXFzLGMoMCwxKSkgIyBEZWZpbmluZyBGcmVxdWVuY3kNCiAgICAgIH0gZWxzZSB7DQogICAgICAgIGZyZXFzID0gcmJpbmQoZnJlcXMsYygxLDApKSAjIERlZmluaW5nIEZyZXF1ZW5jeSANCiAgICAgIH0NCiAgICAgIGQgPSBkKzENCiAgICB9DQogIH0NCiAgDQogIGlmKGlzLm51bGwoZW1vamlbW2ldXSk9PUYpew0KICAgIGUgPSB0YWJsZShlbW9qaVtpXSkNCiAgICANCiAgICAjQnVpbGRpbmcgZnJlcXVlbmNpZXMgZm9yIGVtb2ppcw0KICAgIGZvciAoaiBpbiAxOmxlbmd0aChlKSkgew0KICAgICAgY29uZCA9IChEaWN0ID09IG5hbWVzKGUpW2pdKQ0KICAgICAgaWYgIChhbnkoY29uZCkpIHsNCiAgICAgICAgd2ggPSB3aGljaChjb25kKQ0KICAgICAgICBmcmVxc1t3aCsxLCh5W2ldKzEpXSA9IGZyZXFzW3doKzEsKHlbaV0rMSldK2Vbal0gDQogICAgICAgICMgQWRkaW5nIEZyZXF1ZW5jeQ0KICAgICAgfSBlbHNlIGlmKGlzLm51bGwobmFtZXMoZSkpPT1GKSB7DQogICAgICAgIERpY3RbZF0gPSBuYW1lcyhlKVtqXQ0KICAgICAgICBpZiAoeVtpXT09MSkgew0KICAgICAgICAgIGZyZXFzID0gcmJpbmQoZnJlcXMsYygwLDEpKSAjIERlZmluaW5nIEZyZXF1ZW5jeQ0KICAgICAgICB9IGVsc2Ugew0KICAgICAgICAgIGZyZXFzID0gcmJpbmQoZnJlcXMsYygxLDApKSAjIERlZmluaW5nIEZyZXF1ZW5jeSANCiAgICAgICAgfQ0KICAgICAgICANCiAgICAgICAgZCA9IGQrMQ0KICAgICAgfQ0KICAgIH0NCiAgfQ0KICANCiAgc2V0VHh0UHJvZ3Jlc3NCYXIocGIsaSkNCn0NCkRpY3Rpb25hcnk9bGlzdCgiV29yZHMiPURpY3QsIkZyZXF1ZW5jaWVzIj1mcmVxc1stMSxdKQ0KICByZXR1cm4oRGljdGlvbmFyeSkNCn0NCmBgYA0KDQoNCkZpbmFsbHksIGEgZnVuY3Rpb24gdGhhdCB1c2VzIHRoZSBkaWN0aW9uYXJ5IHRvIGV4dHJhY3QgZmVhdHVyZXMuDQpgYGB7cn0NCkV4dHJhY3RGZWF0dXJlcyA8LSBmdW5jdGlvbihjb3JwdXMsIGRpY3Qpew0KICANCiAgdGV4ID0gY29ycHVzJHRleHQNCiAgZW1vamkgPSBjb3JwdXMkZW1vamkNCiAgDQogIFdvcmRzPWRpY3QkV29yZHMNCiAgZGljdCRGcmVxdWVuY2llcyA9IGRpY3QkRnJlcXVlbmNpZXMvcm93U3VtcyhkaWN0JEZyZXF1ZW5jaWVzKQ0KICBGcmVxdWVuY2llcz1kaWN0JEZyZXF1ZW5jaWVzDQogIA0KICAjUHJvZ3Jlc3MgYmFyDQogIG4gPSBsZW5ndGgodGV4KQ0KICBwYiA9IHR4dFByb2dyZXNzQmFyKG1pbiA9IDAsIG1heCA9IG4sIGluaXRpYWwgPSAwLCBzdHlsZSA9IDMsDQp3aWR0aCA9IDUwLCBjaGFyID0gIj0iKQ0KICANCiAgIHggPSBtYXRyaXgocmVwKDAsbiozKSxucm93ID1uICxuY29sPTMpDQogIGZvciAoaSBpbiAxOm4pIHsNCiAgICB3b3JkbGlzdCA9IG5hbWVzKHNvcnQodGFibGUodW5saXN0KHN0cnNwbGl0KHRleFtpXSwgIiAiKSkpLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgZGVjcmVhc2luZyA9IFRSVUUpKQ0KICAgIGlmKGlzLm51bGwoZW1vamlbW2ldXSk9PUYpe2FwcGVuZCh3b3JkbGlzdCx0YWJsZShlbW9qaVtpXSkpfQ0KICAgIHhbaSwxXSA9IDEgI2JpYXMgdGVybSBpcyBzZXQgdG8gMQ0KICAgICMgbG9vcCB0aHJvdWdoIGVhY2ggd29yZCBpbiB0aGUgbGlzdCBvZiB3b3Jkcw0KICAgIGZvciAod29yZCBpbiB3b3JkbGlzdCl7IA0KICAgICAgaWYgKGFueShXb3Jkcz09d29yZCkpIHsNCiAgICAgICAgIyBpbmNyZW1lbnQgdGhlIHdvcmQgY291bnQgZm9yIHRoZSBuZXV0cmFsIGxhYmVsIDANCiAgICAgICAgeFtpLDJdID0geFtpLDJdK0ZyZXF1ZW5jaWVzW3doaWNoKFdvcmRzPT13b3JkKSwxXQ0KICAgICAgICMgaW5jcmVtZW50IHRoZSB3b3JkIGNvdW50IGZvciB0aGUgcG9zaXRpdmUgbGFiZWwgMQ0KICAgICAgICB4W2ksM10gPSB4W2ksM10rRnJlcXVlbmNpZXNbd2hpY2goV29yZHM9PXdvcmQpLDJdDQogICAgICB9DQogICAgfQ0KICAgIHNldFR4dFByb2dyZXNzQmFyKHBiLGkpDQogIH0NCiByZXR1cm4oeCkgDQp9DQpgYGANCg0KDQpVc2luZyB0aGUgZnVuY3Rpb25zIGRlZmluZWQsIHdlIGFyZSBnb2luZyB0byBjbGVhbiBvdXIgUGVyc2lhbiB0ZXh0LCB0aGVuIGJ1aWxkIGEgZGljdGlvbmFyeSwgYW5kIGV4dHJhY3QgZmVhdHVyZXMgZnJvbSB0aGUgdGV4dC4NCmBgYHtyIG1lc3NhZ2U9RkFMU0UsIHdhcm5pbmc9RkFMU0UsIHJlc3VsdHM9RkFMU0V9DQpUZXh0QWQ9UmVmaW5lVGV4dChUZXh0KQ0KRGljdGlvbmFyeT1CdWlsZEZyZXFzKGxpc3QoInRleHQiPVRleHRBZCwiZW1vamkiPUVtb2ppKSxZKQ0KWCA9IEV4dHJhY3RGZWF0dXJlcyhsaXN0KCJ0ZXh0Ij1UZXh0QWQsImVtb2ppIj1FbW9qaSksIERpY3Rpb25hcnkpDQpgYGANCg0KDQojIENoZWNraW5nIHRoZSBmZWF0dXJlcyBpbnRlcnByZXRhYmlsaXR5DQpUbyBzZWUsIHdlYXRoZXIgaXQgaXMgcG9zc2libGUgdG8gdG8gY2xhc3NpZnkgb3VyIHRleHQgdXNpbmcgdGhlIGZlYXR1cmVzIHdlIGJ1aWx0LCBXZSBtdXN0IGNoZWNrIHRoZSBiZWxvdyBzY2F0dGVyIHBsb3QuDQpgYGB7ciBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQ0KbXlsYWJlbCA8LSBjKCJUcmFpbiBQb3MiLCAiVHJhaW4gTmVnIikNCmNvbG9ycyA8LSBjKCJibHVlIiwgInJlZCIpDQp4bGFiZWwgPSAiU3VtIG9mIE5lZ2F0aXZlIFdvcmRzIg0KeWxhYmVsID0gIlN1bSBvZiBQb3NpdGl2ZSBXb3JkcyINCnBsb3RfbHkoeD1YWywyXSx5PVhbLDNdLA0KICAgICAgICBzeW1ib2xzID0gYygneCcsJ28nKSwNCiAgICAgICAgbWFya2VyID0gbGlzdChzaXplID0gMTApLA0KICAgICAgICB0ZXh0PSBjKDE6bGVuZ3RoKFkpKSwgaG92ZXJpbmZvID0gInRleHQiLA0KICAgICAgICB0eXBlPSJzY2F0dGVyIixtb2RlPSJtYXJrZXJzIiwgY29sb3I9YXMuZmFjdG9yKFkpLGNvbG9ycyA9IGMoInJlZCIsICAiZ3JlZW4iKSkgJT4lDQogICAgICAgIGxheW91dCh0aXRsZSA9ICJDb21tZW50cyBieSB0aGUgRmVhdHVyZXMgYnVpbHQiLA0KICAgICAgICAgICAgICAgYXV0b3NpemUgPSBGLCB3aWR0aCA9IDkyMCwgaGVpZ2h0ID0gNTAwKQ0KYGBgDQoNCg0KDQpUaGUgZmlyc3QgY2xhc3NpZmljYXRpb24gbWV0aG9kIHRoYXQgaXMgd2lkZWx5IGJlaW5nIHVzZWQgYXMgYSB0cml2aWFsIGFuZCBlYXN5LXRvLWltcGxlbWVudCBtZXRob2RvbG9neSBpcyBOYWl2ZSBCYXllcy4gSW4gdGhlIGZvbGxvd2luZyBsaW5lcyBvZiBjb2RlLCB3ZSBhcmUgZ29pbmcgdG8gYnVpbGQgdHdvIGZ1bmN0aW9ucyBieSB3aGljaCB3ZSBjYW4gdXNlIHRoaXMgbWV0aG9kIGZvciBvdXIgZGF0YS4NCmBgYHtyfQ0KRXh0cmFjdFNlbnNlPWZ1bmN0aW9uKGNvcnB1cywgZGljdCl7DQogIA0KICB0ZXggPSBjb3JwdXMkdGV4dA0KICBlbW9qaSA9IGNvcnB1cyRlbW9qaQ0KICANCiAgV29yZHM9ZGljdCRXb3Jkcw0KICBGcmVxdWVuY2llcz1kaWN0JEZyZXF1ZW5jaWVzDQogIA0KICBkPWxlbmd0aChkaWN0JFdvcmRzKQ0KICBuPWxlbmd0aCh0ZXgpDQogIA0KICAjTGFwbGFjaWFuIFNtb290aGluZw0KICBGcmVxcz0xLyhjb2xTdW1zKEZyZXF1ZW5jaWVzKStkKSoodChGcmVxdWVuY2llcykrMSkNCiAgRnJlcXM9dChGcmVxcykNCiAgDQogIHBfd19wb3M9RnJlcXNbLDJdDQogIHBfd19uZWc9RnJlcXNbLDFdDQogIA0KICBsb2dsaWtlbGlob29kID0gbG9nKHBfd19wb3MvcF93X25lZykNCiAgDQogIHA9cmVwKDAsbikNCiAgDQogIHBiID0gdHh0UHJvZ3Jlc3NCYXIobWluID0gMCwgbWF4ID0gbiwgaW5pdGlhbCA9IDAsIHN0eWxlID0gMywNCndpZHRoID0gNTAsIGNoYXIgPSAiPSIpDQogIA0KICBmb3IoaSBpbiAxOm4pew0KICAgIA0KICAgIHdvcmRsaXN0ID0gc29ydCh0YWJsZSh1bmxpc3Qoc3Ryc3BsaXQodGV4W2ldLCAiICIpKSksIGRlY3JlYXNpbmcgPSBUUlVFKQ0KICAgIGlmKGlzLm51bGwoZW1vamlbW2ldXSk9PUYpe2FwcGVuZCh3b3JkbGlzdCx0YWJsZShlbW9qaVtpXSkpfQ0KICAgIA0KICAgIGZvciAoaiBpbiAxOmxlbmd0aCh3b3JkbGlzdCkpew0KICAgICAgaWYgIChhbnkoV29yZHMgPT0gbmFtZXMod29yZGxpc3QpW2pdKSkgew0KICAgICAgICBwW2ldPXBbaV0rd29yZGxpc3Rbal0qbG9nbGlrZWxpaG9vZFtXb3JkcyA9PSBuYW1lcyh3b3JkbGlzdClbal1dDQogICAgICB9DQogICAgfQ0KICAgIHNldFR4dFByb2dyZXNzQmFyKHBiLGkpDQogIH0NCiAgcmV0dXJuKHApDQp9DQpgYGANCg0KDQpBbmQsIHRoZSBwcmVkaWN0aW9yIGZ1bmN0aW9uIHdpbGwgYmUgZGVmaW5lZCBsaWtlIHRoaXM6DQpgYGB7ciB3YXJuaW5nPUZBTFNFLCBpbmNsdWRlPUZBTFNFfQ0KTmFpdmVCYXllc1ByZWRpY3RvcjwtZnVuY3Rpb24ocCx5KXsNCiAgDQogIERfcG9zPXN1bSh5PT0xKQ0KICBEX25lZz1zdW0oeT09MCkNCiAgDQogIGxvZ3ByaW9yID0gbG9nKERfcG9zL0RfbmVnKQ0KICANCiAgcD1wK3JlcChsb2dwcmlvcixsZW5ndGgocCkpDQogIA0KICBpZiAobGVuZ3RoKHApPT1sZW5ndGgoeSkpew0KICAgIHloYXQgPSAocD4wKQ0KICAgIGFjY3VyYWN5ID0gbWVhbih5PT15aGF0KQ0KICB9DQogIA0KICByZXR1cm4obGlzdCgiWWhhdCI9eWhhdCwiQWNjdXJhY3kiPWFjY3VyYWN5KSkNCn0NCmBgYA0KDQoNCk5vdywgd2Ugd2FudCB0byB1c2UgdGhpcyBtZXRob2Qgb24gb3VyIGRhdGEgdG8gYnVpbGQgYSBmZWF0dXJlIGNhbGxlZCAiU2Vuc2UiIHRoYXQgbWVhc3VyZXMgdGhlIGludGVuc2l0eSBvZiBlYWNoIGNvbW1lbnQgYWxvbmdzaWRlIHRoZWlyIHBvbGFyaXR5Lg0KYGBge3IgZWNobz1UUlVFLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFLCByZXN1bHRzPUZBTFNFfQ0KU2Vuc2U9RXh0cmFjdFNlbnNlKGxpc3QoInRleHQiPVRleHRBZCwiZW1vamkiPUVtb2ppKSwgRGljdGlvbmFyeSkNCmBgYA0KDQoNCldlIGNhbiBzZWUgYmVsb3cgaG93IHRoaXMgZmVhdHVyZSBsb29rcyBsaWtlLg0KYGBge3Igd2FybmluZz1GQUxTRX0NCnBsb3RfbHkoeD0xOmxlbmd0aChTZW5zZSkseT1TZW5zZSwgdHlwZSA9ICdiYXInKSAlPiUNCiAgbGF5b3V0KHRpdGxlID0gIk92ZXJhbCBQb2xhcml0eSBvZiBBbGwgdGhlIENvbW1lbnRzIiwNCiAgICAgICAgIHBsb3RfYmdjb2xvcj0nI2U1ZWNmNicsYXV0b3NpemUgPSBULCB3aWR0aCA9IDg4MCwgaGVpZ2h0ID0gNTAwKQ0KYGBgDQoNCg0KVGhlIGFjY3VyYWN5IG9mIE5haXZlIEJheWVzIGNsYXNzaWZpZXIgb24gdGhlIHRyYWluIGRhdGFzZXQgY2FuIGJlIGNhbGN1bGF0ZWQgYXMgZm9sbG93Lg0KYGBge3J9DQpOQj1OYWl2ZUJheWVzUHJlZGljdG9yKFNlbnNlLFkpDQp5aGF0PU5CJFloYXQNCk5CQWNjdXJhY3k9YygpDQpOQkFjY3VyYWN5WyJQb3NpdGl2ZSJdID0gDQogIG1lYW4obmEub21pdChZW1k9PTFdPT15aGF0W1k9PTFdKSkNCk5CQWNjdXJhY3lbIk5lZ2F0aXZlIl0gPSANCiAgbWVhbihuYS5vbWl0KFlbWT09MF09PXloYXRbWT09MF0pKQ0KY2F0KHBhc3RlMCgiT3ZlcmFsIEFjY3VyYWN5OiBcbiIpLG1lYW4oeWhhdD09WSksDQogICAgcGFzdGUwKCJcbkFjY3VyYWN5IGZvciBkaWZmZXJlbnQgc2VudGltZW50czogXG4iKSwgbmFtZXMoTkJBY2N1cmFjeSksDQogICAgcGFzdGUwKCIgXG4iKSxOQkFjY3VyYWN5KQ0KYGBgDQoNCg0KVGhlIHNlY29uZCBjbGFzc2lmaWVyIGlzIGJhc2VkIG9uIHZlY3RvciBzcGFjZSBtb2RlbHMuIFdlIGFyZSBnb2luZyB0byBidWlsZCBzdWNoIGEgY2xhc3NpZmllciBpbiB0aGUgZm9sbG93aW5nIGxpbmVzIG9mIGNvZGUuDQpgYGB7cn0NClZlY3RvclNwYWNlTW9kZWw9IGZ1bmN0aW9uKHgseSl7DQogIA0KICBwb3NfY2VudGVyPWNvbE1lYW5zKHhbeT09MSxdKQ0KICBuZWdfY2VudGVyPWNvbE1lYW5zKHhbeT09MCxdKQ0KICBjZW50ZXJzPWNiaW5kKHBvc19jZW50ZXJbLTFdLG5lZ19jZW50ZXJbLTFdKQ0KICBkb3RfcHJvZHM9eFssLTFdJSolY2VudGVycw0KICBub3JtWD1hcHBseSh4WywtMV0sMSxmdW5jdGlvbih4KShzcXJ0KHN1bSh4XjIpKSkpDQogIG5vcm1DZW50ZXI9YXBwbHkoY2VudGVycywyLGZ1bmN0aW9uKHgpKHNxcnQoc3VtKHheMikpKSkNCiAgDQogIGFuZ2VsX2Nvc2luZT1hcy5tYXRyaXgoMS9ub3JtWCklKiV0KGFzLm1hdHJpeCgxL25vcm1DZW50ZXIpKSooZG90X3Byb2RzKQ0KICANCiAgeWhhdD1hcHBseShhbmdlbF9jb3NpbmUsIDEsIGZ1bmN0aW9uKHgpIHdoaWNoKG1heCh4KT09eCkpDQogIHloYXRbeWhhdD09Ml09MA0KICByZXR1cm4obGlzdCgiWWhhdCI9eWhhdCwiQ2VudGVycyI9Y2VudGVycykpDQp9DQpgYGANCg0KDQpUaGUgbmV4dCBmdW5jdGlvbiBpcyBnb2luZyB0byBjYWxjdWxhdGVzIChwcmVkaWN0cykgdGhlIHBvbGFyaXR5IGZvciBlYWNoIGNvbW1lbnQuIFBsdXMsIGlmIHdlIGlucHV0IHRoZSBkZXBlbmRlbnQgdmFyaWFibGUgKHkpIGFzIHdlbGwgaXQgZ2l2ZXMgdXMgdGhlIGFjY3VyYWN5IG9mIHRoZSBwcmVkaWN0aW9ucy4NCmBgYHtyfQ0KVmVjdG9yU3BhY2VQcmVkaWN0b3I9IGZ1bmN0aW9uKHgseT1OQSxjZW50ZXJzKXsNCiAgZG90X3Byb2RzPXhbLC0xXSUqJWNlbnRlcnMNCiAgbm9ybVg9YXBwbHkoeFssLTFdLDEsZnVuY3Rpb24oeCkoc3FydChzdW0oeF4yKSkpKQ0KICBub3JtQ2VudGVyPWFwcGx5KGNlbnRlcnMsMixmdW5jdGlvbih4KShzcXJ0KHN1bSh4XjIpKSkpDQogIA0KICBhbmdlbF9jb3NpbmU9YXMubWF0cml4KDEvbm9ybVgpJSoldChhcy5tYXRyaXgoMS9ub3JtQ2VudGVyKSkqKGRvdF9wcm9kcykNCiAgDQogIHloYXQ9YXBwbHkoYW5nZWxfY29zaW5lLCAxLCBmdW5jdGlvbih4KSB3aGljaChtYXgoeCk9PXgpKQ0KICB5aGF0W3loYXQ9PTJdPTANCiAgDQogIGFjY3VyYWN5PSBOQQ0KICANCiAgaWYoaXMubmEoeSk9PUZBTFNFICYmIGxlbmd0aCh5KT09bGVuZ3RoKHloYXQpKXsNCiAgICBhY2N1cmFjeSA9IG1lYW4oeT09eWhhdCkNCiAgfQ0KICByZXR1cm4obGlzdCgiWWhhdCI9eWhhdCwiQWNjdXJhY3kiPWFjY3VyYWN5KSkNCn0NCmBgYA0KDQoNClRyYWluIGFjY3VyYWN5IGZvciBWZWN0b3IgU3BhY2UgY2xhc3NpZmllciBpcyBhcyBmb2xsb3dzLg0KYGBge3Igd2FybmluZz1GQUxTRX0NCmZpdFZTPVZlY3RvclNwYWNlTW9kZWwoWCxZKQ0KeWhhdD1maXRWUyRZaGF0DQpWU0FjY3VyYWN5PWMoKQ0KVlNBY2N1cmFjeVsiUG9zaXRpdmUiXSA9IA0KICBtZWFuKG5hLm9taXQoWVtZPT0xXT09eWhhdFtZPT0xXSkpDQpWU0FjY3VyYWN5WyJOZWdhdGl2ZSJdID0gDQogIG1lYW4obmEub21pdChZW1k9PTBdPT15aGF0W1k9PTBdKSkNCmNhdChwYXN0ZTAoIk92ZXJhbCBBY2N1cmFjeTogXG4iKSxtZWFuKHloYXQ9PVkpLA0KICAgIHBhc3RlMCgiXG5BY2N1cmFjeSBmb3IgZGlmZmVyZW50IHNlbnRpbWVudHM6IFxuIiksIG5hbWVzKFZTQWNjdXJhY3kpLA0KICAgIHBhc3RlMCgiIFxuIiksVlNBY2N1cmFjeSkNCmBgYA0KDQoNClRoZSBsb2dpc3RpYyByZWdyZXNzaW9uIGNsYXNzaWZpZXIgYWNjdXJhY3kgb24gdGhpcyBkYXRhc2V0IGNhbiBiZSBjYWxjdWxhdGVkIGFzIGZvbGxvdy4NCmBgYHtyIHdhcm5pbmc9RkFMU0V9DQpmaXRHTE09Z2xtKFl+WFssLTFdLGZhbWlseT0iYmlub21pYWwiKQ0KeWhhdD1yb3VuZChwcmVkaWN0KGZpdEdMTSxhcy5kYXRhLmZyYW1lKFgpLHR5cGU9InJlc3BvbnNlIiksMCkNCkdMTUFjY3VyYWN5PWMoKQ0KR0xNQWNjdXJhY3lbIlBvc2l0aXZlIl0gPSANCiAgbWVhbihuYS5vbWl0KFlbWT09MV09PXloYXRbWT09MV0pKQ0KR0xNQWNjdXJhY3lbIk5lZ2F0aXZlIl0gPSANCiAgbWVhbihuYS5vbWl0KFlbWT09MF09PXloYXRbWT09MF0pKQ0KY2F0KHBhc3RlMCgiT3ZlcmFsIEFjY3VyYWN5OiBcbiIpLG1lYW4oeWhhdD09WSksDQogICAgcGFzdGUwKCJcbkFjY3VyYWN5IGZvciBkaWZmZXJlbnQgc2VudGltZW50czogXG4iKSwgbmFtZXMoR0xNQWNjdXJhY3kpLA0KICAgIHBhc3RlMCgiIFxuIiksR0xNQWNjdXJhY3kpDQpgYGANCkl0IGlzIG5vdCBvYnZpb3VzIHdoaWNoIG1ldGhvZCBwZXJmb3JtcyBiZXR0ZXIgb24gdGhpcyBkYXRhc2V0LiBTbywgd2UgcnVuIGEgc2ltdWxhdGlvbiBvZiAxMDAwIGl0ZXJhdGlvbnMgZWFjaCB0aW1lIHdlIGFyZSBnb2luZyB0byByYW5kb21seSBzcGxpdCB0aGUgZGF0YXNldCBpbnRvIHRyYWluIGFuZCB0ZXN0LiBUaGVuIHdlIGJ1aWxkIG91ciBtb2RlbHMgYmFzZWQgb24gdGhlIHRyYWluIHNldHMgYW5kIHRlc3QgdGhlaXIgcGVyZm9ybWFuY2VzIG9uIHRoZSB0ZXN0IHNldHMuIHRoZSByZXN1bHRzIHdlIGJlIHN0b3JlZCBpbiBhIG1hdHJpeCBhbmQgYWZ0ZXIgdGhlIHNpbXVsYXRpb24gd2lsbCBiZSB2aXN1YWxpemVkLg0KDQoNClNvLCB0aGUgc2ltdWxhdGlvbiBmb3IgdGhlIGRlZmluZWQgbWV0aG9kb2xvZ2llcyBpcyBnb2luZyB0byBiZSBsaWtlIHRoaXMuDQpgYGB7ciB3YXJuaW5nPUZBTFNFLCBpbmNsdWRlPUZBTFNFfQ0KUG9zWCA9IFhbWT09MSxdDQpOZWdYID0gWFtZPT0wLF0NClBvc1NlbnNlID0gU2Vuc2VbWT09MV0NCk5lZ1NlbnNlID0gU2Vuc2VbWT09MF0NCm5QPWRpbShQb3NYKVsxXQ0Kbk49ZGltKE5lZ1gpWzFdDQpBY2N1cmFjaWVzPTANCkVycm9yPW1hdHJpeChyZXAoMCwzKjEwMDApLG5jb2w9MykNCmZvcihpdGVyIGluIDE6MTAwMCl7DQogIGluZGV4UCA9IHNhbXBsZShuUCxjZWlsaW5nKG5QKjAuOSkscmVwbGFjZSA9IEYpDQogIGluZGV4TiA9IHNhbXBsZShuTixjZWlsaW5nKG5OKjAuOSkscmVwbGFjZSA9IEYpDQogIFRyYWluWFBvcyA9IFBvc1hbaW5kZXhQLF0NCiAgVHJhaW5YTmVnID0gTmVnWFtpbmRleE4sXQ0KICBUZXN0WFBvcyA9IFBvc1hbLWluZGV4UCxdDQogIFRlc3RYTmVnID0gTmVnWFstaW5kZXhOLF0NCiAgDQogIFRlc3RTZW5zZVBvcyA9IGFzLm1hdHJpeChQb3NTZW5zZVstaW5kZXhQXSkNCiAgVGVzdFNlbnNlTmVnID0gYXMubWF0cml4KE5lZ1NlbnNlWy1pbmRleE5dKQ0KICANCiAgU2Vuc2VUZXN0ID0gYXBwZW5kKFRlc3RTZW5zZVBvcyxUZXN0U2Vuc2VOZWcpDQogIA0KICBYVHJhaW4gPSByYmluZChUcmFpblhQb3MsVHJhaW5YTmVnKQ0KICBYVGVzdCA9IHJiaW5kKFRlc3RYUG9zLFRlc3RYTmVnKQ0KICBUcmFpblkgPSBjKHJlcCgxLGRpbShUcmFpblhQb3MpWzFdKSxyZXAoMCxkaW0oVHJhaW5YTmVnKVsxXSkpDQogIFRlc3RZID0gYyhyZXAoMSxkaW0oVGVzdFhQb3MpWzFdKSxyZXAoMCxkaW0oVGVzdFhOZWcpWzFdKSkNCiAgZml0VlM9VmVjdG9yU3BhY2VNb2RlbChYVHJhaW4sVHJhaW5ZKQ0KICBWUz1WZWN0b3JTcGFjZVByZWRpY3RvcihYVGVzdCxUZXN0WSxmaXRWUyRDZW50ZXJzKQ0KICANCiAgTkI9TmFpdmVCYXllc1ByZWRpY3RvcihTZW5zZVRlc3QsVGVzdFkpDQogIA0KICBmaXRHTE09Z2xtKFRyYWluWX5YVHJhaW5bLC0xXSxmYW1pbHk9ImJpbm9taWFsIikNCiAgbGFiZWxHTE09cHJlZGljdChmaXRHTE0sYXMuZGF0YS5mcmFtZShYVGVzdCksdHlwZT0icmVzcG9uc2UiKQ0KICANCiAgRXJyb3JbaXRlcixdPWMoMS1WUyRBY2N1cmFjeSwxLU5CJEFjY3VyYWN5LDEtKG1lYW4ocm91bmQobGFiZWxHTE0sMCk9PVRlc3RZKSkpDQp9DQpgYGANCg0KDQpTbywgaW4gdGhlIGZvbGxvd2luZyBsaW5lcyBvZiBjb2RlLCB3ZSBhcmUgZ29pbmcgdG8gYnVpbGQgYSBib3ggcGxvdCB0byBjb21wYXJlIHRoZSBlcnJvciBvZiBlYWNoIG1ldGhvZC4NCmBgYHtyIHdhcm5pbmc9RkFMU0V9DQpmaWcgPC0gcGxvdF9seSh0eXBlID0gJ2JveCcpDQpmaWcgPC0gZmlnICU+JSBhZGRfYm94cGxvdCh5ID0gRXJyb3JbLDFdLCBqaXR0ZXIgPSAwLjMsIHBvaW50cG9zID0gLTEuOCwgYm94cG9pbnRzID0gJ2FsbCcsDQogICAgICAgICAgICAgIG1hcmtlciA9IGxpc3QoY29sb3IgPSAncmdiKDcsNDAsODkpJyksDQogICAgICAgICAgICAgIGxpbmUgPSBsaXN0KGNvbG9yID0gJ3JnYig3LDQwLDg5KScpLA0KICAgICAgICAgICAgICBuYW1lID0gIlZlY3RvciBTcGNhZSBDbGFzc2lmaWVyIikNCmZpZyA8LSBmaWcgJT4lIGFkZF9ib3hwbG90KHkgPSBFcnJvclssMl0sIGppdHRlciA9IDAuMywgcG9pbnRwb3MgPSAtMS44LCBib3hwb2ludHMgPSAnYWxsJywNCiAgICAgICAgICAgICAgbWFya2VyID0gbGlzdChjb2xvciA9ICdyZ2IoOSw1NiwxMjUpJyksDQogICAgICAgICAgICAgIGxpbmUgPSBsaXN0KGNvbG9yID0gJ3JnYig5LDU2LDEyNSknKSwNCiAgICAgICAgICAgICAgbmFtZSA9ICJOYWl2ZSBCYXllcyBDbGFzc2lmaWVyIikNCmZpZyA8LSBmaWcgJT4lIGFkZF9ib3hwbG90KHkgPSBFcnJvclssM10sIGppdHRlciA9IDAuMywgcG9pbnRwb3MgPSAtMS44LCBib3hwb2ludHMgPSAnYWxsJywNCiAgICAgICAgICAgICAgbWFya2VyID0gbGlzdChjb2xvciA9ICdyZ2IoMTEsNjQsMTUwKScpLA0KICAgICAgICAgICAgICBsaW5lID0gbGlzdChjb2xvciA9ICdyZ2IoMTEsNjQsMTUwKScpLA0KICAgICAgICAgICAgICBuYW1lID0gIkxvZ2lzdGljIFJlZ3Jlc3Npb24gQ2xhc3NpZmllciIpDQpmaWcgJT4lIGxheW91dChhdXRvc2l6ZSA9IEYsIHdpZHRoID0gOTIwLCBoZWlnaHQgPSA1MDApDQpgYGANCkl0IHNlZW1zIG9idmlvdXMgbm93IHRoYXQgdmVjdG9yIHNwYWNlIGNsYXNzaWZpZXIgcGVyZm9ybXMgc2xpZ2h0bHkgYmV0dGVyIHRoYW4gbmFpdmUgQmF5ZXMgYW5kIHNpZ25pZmljYW50bHkgYmV0dGVyIHRoYW4gTG9naXN0aWMgcmVncmVzc2lvbiBjbGFzc2lmaWVyLg==